/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.oozie.authentication;

import java.io.IOException;
import java.util.Properties;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.hadoop.security.authentication.client.AuthenticationException;
import org.apache.hadoop.security.authentication.server.AuthenticationToken;
import org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler;

// TODO: Delete this class when hadoop's AltKerberosAuthenticationHandler becomes available in a hadoop release
/**
 * The {@link AltKerberosAuthenticationHandler} behaves exactly the same way as the KerberosAuthenticationHandler, except that it
 * allows for an alternative form of authentication for browsers while still using Kerberos for Java access. This is an abstract
 * class that should be subclassed to allow a developer to implement their own custom authentication for browser access. The
 * alternateAuthenticate method will be called whenever a request comes from a browser.
 * <p/>
 */
public abstract class AltKerberosAuthenticationHandler extends KerberosAuthenticationHandler {

    /**
     * Constant that identifies the authentication mechanism.
     */
    public static final String TYPE = "alt-kerberos";

    /**
     * Constant for the configuration property that indicates which user agents are not considered browsers (comma separated)
     */
    public static final String NON_BROWSER_USER_AGENTS = TYPE + ".non-browser.user-agents";
    private static final String NON_BROWSER_USER_AGENTS_DEFAULT = "java,curl,wget,perl";

    private String[] nonBrowserUserAgents;

    /**
     * Returns the authentication type of the authentication handler, 'alt-kerberos'.
     * <p/>
     *
     * @return the authentication type of the authentication handler, 'alt-kerberos'.
     */
    @Override
    public String getType() {
        return TYPE;
    }

    @Override
    public void init(Properties config) throws ServletException {
        super.init(config);

        nonBrowserUserAgents = config.getProperty(NON_BROWSER_USER_AGENTS, NON_BROWSER_USER_AGENTS_DEFAULT).split("\\W*,\\W*");
        for (int i = 0; i < nonBrowserUserAgents.length; i++) {
            nonBrowserUserAgents[i] = nonBrowserUserAgents[i].toLowerCase();
        }
    }

  /**
     * It enforces the the Kerberos SPNEGO authentication sequence returning an {@link AuthenticationToken} only after the Kerberos
     * SPNEGO sequence has completed successfully (in the case of Java access) and only after the custom authentication implemented
     * by the subclass in alternateAuthenticate has completed successfully (in the case of browser access).
     * <p/>
     *
     * @param request the HTTP client request.
     * @param response the HTTP client response.
     *
     * @return an authentication token if the request is authorized or null
     *
     * @throws IOException thrown if an IO error occurred
     * @throws AuthenticationException thrown if an authentication error occurred
     */
    @Override
    public AuthenticationToken authenticate(HttpServletRequest request, HttpServletResponse response)
            throws IOException, AuthenticationException {
        AuthenticationToken token;
        if (isBrowser(request.getHeader("User-Agent"))) {
            token = alternateAuthenticate(request, response);
        }
        else {
            token = super.authenticate(request, response);
        }
        return token;
    }

    /**
     * This method parses the User-Agent String and returns whether or not it refers to a browser. If its not a browser, then
     * Kerberos authentication will be used; if it is a browser, alternateAuthenticate from the subclass will be used.
     * <p/>
     * A User-Agent String is considered to be a browser if it does not contain any of the values from
     * alt-kerberos.non-browser.user-agents; the default behavior is to consider everything a browser unless it contains one of:
     * "java", "curl", "wget", or "perl". Subclasses can optionally override this method to use different behavior.
     *
     * @param userAgent The User-Agent String, or null if there isn't one
     * @return true if the User-Agent String refers to a browser, false if not
     */
    protected boolean isBrowser(String userAgent) {
        if (userAgent == null) {
            return false;
        }
        userAgent = userAgent.toLowerCase();
        boolean isBrowser = true;
        for (String nonBrowserUserAgent : nonBrowserUserAgents) {
            if (userAgent.contains(nonBrowserUserAgent)) {
                isBrowser = false;
                break;
            }
        }
        return isBrowser;
    }

    /**
     * Subclasses should implement this method to provide the custom authentication to be used for browsers.
     *
     * @param request the HTTP client request.
     * @param response the HTTP client response.
     * @return an authentication token if the request is authorized, or null
     * @throws IOException thrown if an IO error occurs
     * @throws AuthenticationException thrown if an authentication error occurs
     */
    public abstract AuthenticationToken alternateAuthenticate(HttpServletRequest request, HttpServletResponse response)
            throws IOException, AuthenticationException;
}
